/*
 * Tranquil Java Integrated Development Environment
 *
 * The GNU General Public License Version 3
 *
 * Copyright (C) 2021 Autumn Lamonte
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @author Autumn Lamonte [AutumnWalksTheLake@gmail.com] ⚧ Trans Liberation Now
 * @version 1
 */
package tjide.ui;

import java.util.ArrayList;
import java.util.List;
import java.text.MessageFormat;
import java.util.ResourceBundle;

import gjexer.TApplication;
import gjexer.TCommand;
import gjexer.THScroller;
import gjexer.TScrollableWindow;
import gjexer.TVScroller;
import gjexer.TWidget;
import gjexer.bits.CellAttributes;
import gjexer.event.TCommandEvent;
import gjexer.event.TKeypressEvent;
import gjexer.event.TMouseEvent;
import gjexer.event.TResizeEvent;
import static gjexer.TCommand.*;
import static gjexer.TKeypress.*;

import tjide.project.Target;

/**
 * MessageWindow is a permanent window that shows a list of messages that can
 * be clicked on to go to a specific spot in an InternalEditorWindow.
 *
 * Note that MessageWindow is an unusual UI object in that the Message
 * add/remove functions are generally called from a non-UI thread.
 */
public class MessageWindow extends TScrollableWindow {

    /**
     * Translated strings.
     */
    private static ResourceBundle i18n = ResourceBundle.getBundle(MessageWindow.class.getName());

    // ------------------------------------------------------------------------
    // Constants --------------------------------------------------------------
    // ------------------------------------------------------------------------

    private static final TCommand cmViewSource          = new TCommand(1020);
    private static final TCommand cmEditSource          = new TCommand(1021);

    /**
     * The number of lines to scroll on mouse wheel up/down.
     */
    private static final int wheelScrollSize = 3;

    // ------------------------------------------------------------------------
    // Variables --------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * The messages to display.
     */
    private List<Message> messages = new ArrayList<Message>();

    /**
     * The messages queued up to add to the messages list.
     */
    private List<Message> newMessages = new ArrayList<Message>();

    /**
     * If set, clear the messages list.
     */
    private boolean clearMessagesFlag = false;

    // ------------------------------------------------------------------------
    // Constructors -----------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Construct window.
     *
     * @param parent the main application
     */
    public MessageWindow(final TApplication parent) {
        super(parent, i18n.getString("windowTitle"), 0,
            parent.getDesktopBottom() - 11,
            parent.getScreen().getWidth(), 10,
            RESIZABLE | ABSOLUTEXY | HIDEONCLOSE);

        hScroller = new THScroller(this, 17, getHeight() - 2, getWidth() - 20);
        vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
        setMinimumWindowWidth(25);
        setMinimumWindowHeight(10);
        setTopValue(0);
        setBottomValue(0);
        setLeftValue(0);
        setRightValue(getMaxMessageLength() - 1);

        statusBar = newStatusBar(i18n.getString("statusBar"));
        statusBar.addShortcutKeypress(kbF1, cmHelp,
            i18n.getString("statusBarHelp"));
        statusBar.addShortcutKeypress(kbSpace, cmViewSource,
            i18n.getString("statusBarViewSource"));
        statusBar.addShortcutKeypress(kbEnter, cmEditSource,
            i18n.getString("statusBarEditSource"));
        statusBar.addShortcutKeypress(kbF10, cmMenu,
            i18n.getString("statusBarMenu"));

        hide();
    }

    // ------------------------------------------------------------------------
    // Event handlers ---------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Handle window/screen resize events.
     *
     * @param event resize event
     */
    @Override
    public void onResize(final TResizeEvent event) {
        if (event.getType() == TResizeEvent.Type.WIDGET) {
            // Resize the messages
            for (Message message: messages) {
                message.setWidth(getWidth() - 2);
            }

            // Have TScrollableWindow handle the scrollbars
            super.onResize(event);
            return;
        }

        // Pass to children instead
        for (TWidget widget: getChildren()) {
            widget.onResize(event);
        }
    }

    /**
     * Handle mouse press events.
     *
     * @param mouse mouse button press event
     */
    @Override
    public void onMouseDown(final TMouseEvent mouse) {
        // Use TWidget's code to pass the event to the children.
        super.onMouseDown(mouse);

        if (mouse.isMouseWheelUp()) {
            for (int i = 0; i < wheelScrollSize; i++) {
                verticalDecrement();
            }
            alignMessages();
        } else if (mouse.isMouseWheelDown()) {
            for (int i = 0; i < wheelScrollSize; i++) {
                verticalIncrement();
            }
            alignMessages();
        }

        // User clicked on a message, update the scrollbar accordingly.
        for (int i = 0; i < messages.size(); i++) {
            if (messages.get(i).isActive()) {
                setVerticalValue(i);
                return;
            }
        }
    }

    /**
     * Handle mouse release events.
     *
     * @param mouse mouse button release event
     */
    @Override
    public void onMouseUp(final TMouseEvent mouse) {
        // Use TWidget's code to pass the event to the children.
        super.onMouseUp(mouse);

        if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
            // Clicked/dragged on vertical scrollbar
            alignMessages();
        }
        if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
            // Clicked/dragged on horizontal scrollbar
            for (Message message: messages) {
                message.setLeft(getHorizontalValue());
            }
        }

        // User clicked on a message, update the scrollbar accordingly.
        for (int i = 0; i < messages.size(); i++) {
            if (messages.get(i).isActive()) {
                setVerticalValue(i);
                return;
            }
        }
    }

    /**
     * Method that subclasses can override to handle mouse movements.
     *
     * @param mouse mouse motion event
     */
    @Override
    public void onMouseMotion(final TMouseEvent mouse) {
        // Use TWidget's code to pass the event to the children.
        super.onMouseMotion(mouse);

        if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
            // Clicked/dragged on vertical scrollbar
            alignMessages();
        }
        if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
            // Clicked/dragged on horizontal scrollbar
            for (Message message: messages) {
                message.setLeft(getHorizontalValue());
            }
        }
    }

    /**
     * Handle keystrokes.
     *
     * @param keypress keystroke event
     */
    @Override
    public void onKeypress(final TKeypressEvent keypress) {
        if (messages.size() == 0) {
            return;
        }

        if (keypress.equals(kbRight)) {
            horizontalIncrement();
            for (Message message: messages) {
                message.setLeft(getHorizontalValue());
            }
            return;
        }
        if (keypress.equals(kbLeft)) {
            horizontalDecrement();
            for (Message message: messages) {
                message.setLeft(getHorizontalValue());
            }
            return;
        }

        if (keypress.equals(kbDown)) {
            verticalIncrement();
            alignMessages();
            return;
        }
        if (keypress.equals(kbPgDn)) {
            bigVerticalIncrement();
            alignMessages();
            return;
        }
        if (keypress.equals(kbUp)) {
            verticalDecrement();
            alignMessages();
            return;
        }
        if (keypress.equals(kbPgUp)) {
            bigVerticalDecrement();
            alignMessages();
            return;
        }

        if (keypress.equals(kbHome)) {
            setVerticalValue(getTopValue());
            alignMessages();
            return;
        }
        if (keypress.equals(kbEnd)) {
            setVerticalValue(getBottomValue());
            alignMessages();
            return;
        }

        if (keypress.equals(kbEnter)) {
            messages.get(getVerticalValue()).onKeypress(keypress);
            return;
        }

        // Pass it on
        super.onKeypress(keypress);
    }

    /**
     * Handle posted command events.
     *
     * @param command command event
     */
    @Override
    public void onCommand(final TCommandEvent command) {
        if (command.equals(cmViewSource)) {
            Message message = messages.get(getVerticalValue());
            if (message.switchToEditor()) {
                activate();
                return;
            }
        } else if (command.equals(cmEditSource)) {
            Message message = messages.get(getVerticalValue());
            if (message.switchToEditor()) {
                return;
            }
        } else {
            // I didn't take it, pass it on.
            super.onCommand(command);
        }
    }

    // ------------------------------------------------------------------------
    // TWindow ----------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Retrieve the background color.
     *
     * @return the background color
     */
    @Override
    public final CellAttributes getBackground() {
        return getTheme().getColor("messageWindow.background");
    }

    /**
     * Retrieve the border color.
     *
     * @return the border color
     */
    @Override
    public CellAttributes getBorder() {
        if (inWindowMove) {
            return getTheme().getColor("messageWindow.windowMove");
        }
        return getTheme().getColor("messageWindow.background");
    }

    /**
     * Retrieve the color used by the window movement/sizing controls.
     *
     * @return the color used by the zoom box, resize bar, and close box
     */
    @Override
    public CellAttributes getBorderControls() {
        return getTheme().getColor("messageWindow.borderControls");
    }

    // ------------------------------------------------------------------------
    // TScrollableWindow ------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Draw the messages.
     */
    @Override
    public void draw() {
        // Synchronize against newMessages so we are not stomped on by the
        // compiler thread.
        synchronized (newMessages) {
            if (clearMessagesFlag) {
                // Clear the message list as requested.
                getChildren().removeAll(messages);
                messages.clear();
                setTopValue(0);
                setBottomValue(0);
                setLeftValue(0);
                setRightValue(0);
                setHorizontalValue(0);
                setVerticalValue(0);
                clearMessagesFlag = false;
            }

            // Add any messages dispatched from other threads.
            if (newMessages.size() > 0) {

                for (Message message: newMessages) {
                    messages.add(message);
                    message.setParent(this, false);
                }
                newMessages.clear();

                assert (getChildren().size() == messages.size() + 2);
                assert (messages.size() > 0);
                setBottomValue(messages.size() - 1);
                setVerticalValue(getBottomValue());
                setRightValue(getMaxMessageLength() - 1);
                alignMessages();
            }
        }

        // Now resume normal drawing.
        super.draw();
    }

    // ------------------------------------------------------------------------
    // MessageWindow ----------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Make messages visible that match the vertical scrollbar.
     */
    private void alignMessages() {
        int top = getVerticalValue();
        int messagesHeight = 0;
        for (Message message: messages) {
            messagesHeight += message.getHeight();
        }
        if (messagesHeight <= getHeight() - 2) {
            // All messages fit in the window.
            int y = 0;
            for (int i = 0; i < messages.size(); i++) {
                messages.get(i).setEnabled(true);
                messages.get(i).setVisible(true);
                messages.get(i).setY(y);
                y += messages.get(i).getHeight();
            }
            activate(messages.get(getVerticalValue()));
            return;
        }

        /*
         * Some messages will not fit in the window.  Find the number of rows
         * needed to display from the current vertical position to the end:
         *
         * - If this meets or exceeds the available height, then draw from
         *   the vertical position to the number of visible rows.
         *
         * - If this is less than the available height, back up until
         *   meeting/exceeding the height, and draw from there to the end.
         *
         */
        int rowsNeeded = 0;
        for (int i = getVerticalValue(); i <= getBottomValue(); i++) {
            rowsNeeded += messages.get(i).getHeight();
        }
        while (rowsNeeded < getHeight() - 2) {
            // Decrease top until we meet/exceed the visible display.
            if (top == getTopValue()) {
                break;
            }
            top--;
            rowsNeeded += messages.get(top).getHeight();
        }

        // All set, now disable all messages except the visible ones.
        for (Message message: messages) {
            message.setEnabled(false);
            message.setVisible(false);
            message.setY(-1);
        }
        int y = 0;
        for (int i = top; (i <= getBottomValue()) && (y < getHeight() - 2); i++) {
            messages.get(i).setEnabled(true);
            messages.get(i).setVisible(true);
            messages.get(i).setY(y);
            y += messages.get(i).getHeight();
        }
        activate(messages.get(getVerticalValue()));
    }

    /**
     * Get the maximum length of messages.
     *
     * @return the maximum length of messages
     */
    private int getMaxMessageLength() {
        int result = 0;
        for (Message message: messages) {
            result = Math.max(message.getDisplayLength(), result);
        }
        return result;
    }

    /**
     * Add a warning message to the list.
     *
     * @param target the target that generated this message
     * @param line the line number of the message.  1-based: 0 or negative
     * means no relevant line number.
     * @param column the column number of the message.  1-based: 0 or
     * negative means no relevant column number.
     * @param message the message text
     */
    public void addCompileWarning(final Target target, final long line,
        final long column, final String message) {

        synchronized (newMessages) {
            Message newMessage = new Message(this, Message.Type.WARNING, target,
                (int) line, (int) column,
                MessageFormat.format(i18n.getString("warningMessage"),
                    target.getName(), line, column, message));

            newMessages.add(newMessage);
        }
    }

    /**
     * Add an error message to the list.
     *
     * @param target the target that generated this message
     * @param line the line number of the message.  1-based: 0 or negative
     * means no relevant line number.
     * @param column the column number of the message.  1-based: 0 or
     * negative means no relevant column number.
     * @param message the message text
     */
    public void addCompileError(final Target target, final long line,
        final long column, final String message) {

        synchronized (newMessages) {
            Message newMessage = new Message(this, Message.Type.ERROR, target,
                (int) line, (int) column,
                MessageFormat.format(i18n.getString("errorMessage"),
                    target.getName(), line, column, message));

            newMessages.add(newMessage);
        }
    }

    /**
     * Clear messages.
     */
    public void clearMessages() {
        synchronized (newMessages) {
            clearMessagesFlag = true;
        }
    }

    /**
     * Add a compile success message to the list.
     *
     * @param target the target that generated this message
     * @param errorCount number of errors in this compile
     * @param warningCount number of warnings in this compile
     * @param lineCount number of lines in this compile
     */
    public void addCompileSuccessSummary(final Target target,
        final int errorCount, final int warningCount, final int lineCount) {

        synchronized (newMessages) {
            Message newMessage = new Message(this, Message.Type.INFO, target,
                -1, -1,
                MessageFormat.format(i18n.getString("compileSuccessSummary"),
                    (target == null ? i18n.getString("total") : target.getName()),
                    errorCount, warningCount, lineCount));

            newMessages.add(newMessage);
        }
    }

    /**
     * Add a compile failed message to the list.
     *
     * @param target the target that generated this message
     * @param errorCount number of errors in this compile
     * @param warningCount number of warnings in this compile
     * @param lineCount number of lines in this compile
     */
    public void addCompileFailedSummary(final Target target,
        final int errorCount, final int warningCount, final int lineCount) {

        synchronized (newMessages) {
            Message newMessage = new Message(this, Message.Type.ERROR, target,
                -1, -1,
                MessageFormat.format(i18n.getString("compileFailedSummary"),
                    (target == null ? i18n.getString("total") : target.getName()),
                    errorCount, warningCount, lineCount));

            newMessages.add(newMessage);
        }
    }

    /**
     * Switch in the editor to the previous or next error in the messages
     * list.
     *
     * @param forward if true, find the next error message in the list
     */
    public void selectError(final boolean forward) {
        if (messages.size() == 0) {
            return;
        }

        int idx = getVerticalValue();
        do {
            if (forward == true) {
                // Go forward
                idx++;
                if (idx > getBottomValue()) {
                    idx = 0;
                }
            } else {
                // Go backward
                idx--;
                if (idx < getTopValue()) {
                    idx = getBottomValue();
                }
            }
            Message message = messages.get(idx);
            if ((message.getType() == Message.Type.ERROR)
                || (message.getType() == Message.Type.WARNING)
            ) {
                if (message.switchToEditor()) {
                    setVerticalValue(idx);
                    alignMessages();
                    return;
                }
            }
        } while (idx != getVerticalValue());

        // No errors found, do nothing.
    }

}
